﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Principal;
using System.ComponentModel;
using System.Runtime.InteropServices;
using NuGet.Enterprise.Server.Configuration;
using System.Diagnostics;

namespace NuGet.Enterprise.Server
{
    /// <summary> 
    /// Allows code to be executed under the security context of a specified user account. 
    /// </summary> 
    /// <remarks>  
    /// 
    /// Implements IDispose, so can be used via a using-directive or method calls; 
    ///  ... 
    /// 
    ///  using (ImpersonationContext.Impersonate("myUsername", "myDomainname", "myPassword")) 
    ///  { 
    ///   ... 
    ///   [code that executes under the new context] 
    ///   ... 
    ///  } 
    /// 
    ///  ... 
    /// </remarks> 
    internal sealed class UserContext
        : IDisposable
    {
        #region Statics

        private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();

        /// <summary>
        /// Impersonates the specified user account.
        /// </summary>
        /// <param name="credentials">The credentials.</param>
        ///<param name="logonType">Type of the logon.</param>  
        ///<param name="logonProvider">The logon provider. <see cref="Mit.Sharepoint.WebParts.EventLogQuery.Network.LogonProvider"/></param>  
        public static IDisposable Impersonate(BuildStoreCredentials credentials, LogonType logonType, LogonProvider logonProvider)
        {
            if (credentials == null || string.IsNullOrEmpty(credentials.Username) || string.IsNullOrEmpty(credentials.Domain)) return null;

            return Impersonate(credentials.Username, credentials.Domain, credentials.Password, logonType, logonProvider);
        }


        /// <summary>  
        /// Impersonates the specified user account.  
        /// </summary>  
        ///<param name="userName">Name of the user.</param>  
        ///<param name="domainName">Name of the domain.</param>  
        ///<param name="password">The password. <see cref="System.String"/></param>  
        ///<param name="logonType">Type of the logon.</param>  
        ///<param name="logonProvider">The logon provider. <see cref="Mit.Sharepoint.WebParts.EventLogQuery.Network.LogonProvider"/></param>  
        public static IDisposable Impersonate(string userName, string domainName, string password, LogonType logonType, LogonProvider logonProvider)
        {
            using (Stopwatch.StartNew().Monitor((TimeSpan duration) => log.Debug("Creating an impersonated context for '{0}\\{1}' takes {2} msec", domainName, userName, duration.TotalMilliseconds)))
            {
                IntPtr logonToken = IntPtr.Zero;
                IntPtr logonTokenDuplicate = IntPtr.Zero;

                try
                {
                    // revert to the application pool identity, saving the identity of the current requestor    
                    WindowsImpersonationContext wic = WindowsIdentity.Impersonate(IntPtr.Zero);

                    // do logon & impersonate    
                    if (LogonUser(userName, domainName, password, (int)logonType, (int)logonProvider, ref logonToken) != 0)
                    {
                        if (DuplicateToken(logonToken, (int)ImpersonationLevel.SecurityImpersonation, ref logonTokenDuplicate) != 0)
                        {
                            var wi = new WindowsIdentity(logonTokenDuplicate);

                            wi.Impersonate(); // discard the returned identity context (which is the context of the application pool)

                            return new UserContext(wic);
                        }
                        else
                        {
                            throw new Win32Exception(Marshal.GetLastWin32Error());
                        }
                    }
                    else
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }
                }
                finally
                {
                    if (logonToken != IntPtr.Zero)
                    {
                        CloseHandle(logonToken);
                    }

                    if (logonTokenDuplicate != IntPtr.Zero)
                    {
                        CloseHandle(logonTokenDuplicate);
                    }
                }
            }
        }

        #endregion

        #region Fields
        
        private WindowsImpersonationContext _wic;

        #endregion

        #region Methods
        
        /// <summary>  
        /// Initializes a new instance of the <see cref="Impersonator"/> class.  
        /// </summary>  
        private UserContext(WindowsImpersonationContext wic)
        {
            _wic = wic;
        }

        /// <summary>  
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.  
        /// </summary>  
        public void Dispose()
        {
            if (_wic != null) _wic.Undo();

            _wic = null;

            GC.SuppressFinalize(this);
        }

        #endregion

        #region Native

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern int LogonUser(string lpszUserName, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool RevertToSelf();

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public static extern bool CloseHandle(IntPtr handle);

        #endregion

        #region Classes

        public enum ImpersonationLevel
        {
            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3
        }

        public enum LogonProvider
        {
            LOGON32_PROVIDER_DEFAULT = 0,
            LOGON32_PROVIDER_WINNT35 = 1,
            LOGON32_PROVIDER_WINNT40 = 2,
            LOGON32_PROVIDER_WINNT50 = 3
        };

        public enum LogonType
        {
            LOGON32_LOGON_INTERACTIVE = 2,
            LOGON32_LOGON_NETWORK = 3,
            LOGON32_LOGON_BATCH = 4,
            LOGON32_LOGON_SERVICE = 5,
            LOGON32_LOGON_UNLOCK = 7,
            LOGON32_LOGON_NETWORK_CLEARTEXT = 8, // Win2K or higher  
            LOGON32_LOGON_NEW_CREDENTIALS = 9 // Win2K or higher 
        };
        #endregion
    }
}